/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <ppltasks.h>
#include <baja/stream/storage_file.h>
#include <journal/common/automation_service.h>

namespace journal { namespace services { namespace automation {

namespace bs = baja::storage;

automation_service::automation_service() :
    _inProgress(false)
{
}

std::shared_ptr<iautomation_service> automation_service::create()
{
    std::shared_ptr<automation_service> returnValue = std::make_shared<automation_service>();
    returnValue->buildAlphabetSymbols();
    returnValue->buildJorgeSymbol();
    returnValue->buildEricSymbol();
    returnValue->buildAlanSymbol();
    returnValue->buildJulioSymbol();
    returnValue->buildGeorgeSymbol();
    returnValue->buildJorgeSketch0Symbol();
    returnValue->buildJorgeSketch1Symbol();
    return returnValue;
}

void automation_service::registerRootView(std::weak_ptr<baja::ibase> notebookView)
{
    _notebookView = std::dynamic_pointer_cast<journal::view::inotebook>(notebookView.lock());
}

void automation_service::sleep(b::uint32 milliSeconds)
{
    HANDLE sleep = ::CreateEventEx(nullptr, nullptr, 0, EVENT_ALL_ACCESS);
    ::WaitForSingleObjectEx(sleep, milliSeconds, FALSE);
    CloseHandle(sleep);
}

bool automation_service::inProgress()
{
    return _inProgress;
}

void automation_service::getSymbolStrokes(std::wstring symbol, std::vector<symbol_strokes>& strokes)
{
    strokes.clear();
    if (_symbolsMap.end() == _symbolsMap.find(symbol))
    {
        return;
    }
    strokes = _symbolsMap[symbol];
}

void automation_service::inkSymbol(
    const std::wstring& symbol,
    const symbol_context& context,
    const std::shared_ptr<view::iink_surface>& inkSurface)
{
    if (symbol == L" ")
    {
        return;
    }
    std::shared_ptr<view::iink_surface_automation> inkSurfaceAutomation = std::dynamic_pointer_cast<view::iink_surface_automation>(inkSurface);
    std::vector<symbol_strokes> strokes;
    getSymbolStrokes(symbol, strokes);

    for (auto &inkStroke : strokes)
    {
        std::shared_ptr<model::iink_stroke_model> stroke = inkSurface->model()->createInkStroke();
        stroke->setColor(bi::ink_color::black);
        stroke->setWidth(inkStroke.width);

        for (b::uint32 index = 0 ; index < inkStroke.count ;  ++index)
        {
            baja::math::point2d p(inkStroke.points[index].position);

            if (context.default == false)
            {
                p.x *= context.scale;
                p.y *= context.scale;

                p.x += context.x;
                p.y -= context.y;
            }

            stroke->addPoint(
                p,
                inkStroke.points[index].pressure,
                inkStroke.points[index].t1);

        }
        inkSurface->model()->attachInkStroke(stroke);
        inkSurfaceAutomation->generateInk(stroke);
    }
}

b::uint32 automation_service::fillPageWithStrokes(
    const std::shared_ptr<journal::view::inotebook>& notebookView,
    const b::uint32& pageId,
    const b::wchar* text,
    const b::uint32& textIndex,
    const b::uint32& targetStrokeCount,
    const b::uint32& postSymbolPause,
    const b::uint32& fontSize,
    const b::uint32& fontThickness)
{
    std::shared_ptr<view::inotebook_automation> notebookAutomation = std::dynamic_pointer_cast<view::inotebook_automation>(notebookView);
    std::shared_ptr<view::iink_surface> inkSurface = getInkSurface(notebookAutomation->pageFromId(pageId));

    b::float32 strokeToSymbolRatio = 1.15f;
    b::float32 printableToSymbolRatio = 0.7f;

    b::float32 paperWidth = constants::general::pageWorldDimensions().x;
    b::float32 paperHeight = constants::general::pageWorldDimensions().y;
    b::float32 aspectRatio = paperHeight / paperWidth;

    b::uint32 strokeRowCount = static_cast<b::uint32>(sqrt(targetStrokeCount / (aspectRatio * strokeToSymbolRatio * printableToSymbolRatio)));
    b::uint32 strokeColumnCount = static_cast<b::uint32>(strokeRowCount * aspectRatio);

    b::float32 scale = paperWidth / strokeColumnCount * fontSize;
    b::float32 width = 0.1f * scale * fontThickness;

    b::float32 strokeBoxWidth = paperWidth / static_cast<b::float32>(strokeColumnCount);
    b::float32 strokeBoxHeight = paperHeight / static_cast<b::float32>(strokeRowCount);

    b::uint32 textLength = static_cast<b::uint32>(wcslen(text));
    b::uint32 charIndex = textIndex;

    for (b::uint32 row = 0 ; row < strokeRowCount ; ++row)
    {
        for (b::uint32 column = 0 ; column < strokeColumnCount ; ++column)
        {
            b::float32 x = column * ((paperWidth - strokeBoxWidth)/ strokeColumnCount) - (paperWidth / 2) + (strokeBoxWidth / 2);
            b::float32 y = row * ((paperHeight - strokeBoxHeight)/ strokeRowCount ) - (paperHeight / 2) + (strokeBoxHeight / 2); 

            symbol_context symbolContext;
            symbolContext.default = false;
            symbolContext.x = x;
            symbolContext.y = y;
            symbolContext.scale = scale;

            std::wstring symbol;
            symbol += text[charIndex];
            inkSymbol(symbol, symbolContext, inkSurface);
            ++charIndex;
            charIndex %= textLength;

            sleep(postSymbolPause);
        }
    }

    return charIndex;
}

void automation_service::addPhoto(
    const std::shared_ptr<journal::view::inotebook>& notebookView,
    const b::uint32& pageId,
    const std::shared_ptr<bs::istorage_file>& photoFile,
    const b::float32& angle,
    const b::float32& x,
    const b::float32& y,
    const b::float32& scale
    )
{
    std::shared_ptr<view::inotebook_automation> notebookAutomation = std::dynamic_pointer_cast<view::inotebook_automation>(notebookView);
    std::shared_ptr<view::ipage> page = notebookAutomation->pageFromId(pageId);

    page->addPhoto(photoFile, x, y, scale, angle);
}

void automation_service::addPhoto(
    const std::shared_ptr<journal::view::inotebook>& notebookView,
    const b::uint32& pageId,
    const std::wstring& fileName,
    const b::float32& angle,
    const b::float32& x,
    const b::float32& y,
    const b::float32& scale)
{
    std::shared_ptr<bs::istorage_file> photoFile = bs::createStorageFile();
    HANDLE serialize = ::CreateEventEx(nullptr, nullptr, 0, EVENT_ALL_ACCESS);
    photoFile->initializeAsync(
            bs::known_folders::picture_library,
            fileName,
            [=](bool success)
            {
                this->addPhoto(
                    notebookView,
                    pageId,
                    photoFile,
                    angle,
                    x,
                    y,
                    scale
                    );
                SetEvent(serialize);
            }
        );
    ::WaitForSingleObjectEx(serialize, INFINITE, FALSE);
    CloseHandle(serialize);
}

void automation_service::fillPageWithPhotos(
    const std::shared_ptr<journal::view::inotebook>& notebookView,
    const b::uint32& pageId)
{
    b::uint32 maxPhotoCount = constants::page_test_automation_settings::photoCountPerPage();
    b::uint32 postPhotoPause = constants::page_test_automation_settings::postPhotoPause();

    b::float32 width = constants::general::pageWorldDimensions().x;
    b::float32 height = constants::general::pageWorldDimensions().y;

    // (square root of variance of maxPhotoCount is the standard deviation)
    b::uint32 photoCount = static_cast<b::uint32>(abs(baja::randNormal(0, static_cast<b::float32>(sqrt(maxPhotoCount)))));

    DEBUG_CONSOLE_TRACE(boost::wformat(L"automation: photo count: %i") % photoCount);
    b::uint32 sourcePhotoCount = constants::page_test_automation_settings::sourcePhotoCount();

    static bool initialized = false;
    static std::vector<std::wstring> photoFileNames;
    static int photoIndex;
    if (!initialized)
    {
        for (b::uint32 i = 0; i < sourcePhotoCount; i++)
        {
            std::wstring photoFileName = (boost::wformat(L"%i.jpg") % (i)).str();
            photoFileNames.push_back(photoFileName);
        }

        initialized = true;
    }

    // shuffle the photos a bit
    for (int i = 0; i < 5; i++)
    {
        int x = (int)baja::rand(0, (b::float32)sourcePhotoCount-1);
        int y = (int)baja::rand(0, (b::float32)sourcePhotoCount-1);
        std::swap(photoFileNames[x], photoFileNames[y]);
    }

    for (b::uint32 i = 0 ; i < photoCount ; ++i)
    {
        photoIndex = (photoIndex + 1) % sourcePhotoCount;
        b::float32 photoAngle = baja::randNormal(0, 20);
        b::float32 photoX = baja::rand(0, width) - (width * 2 / 3) + (width * 1 / 6);
        b::float32 photoY = baja::rand(0, height) - (height * 2/ 3) + (height * 1 / 6);
        b::float32 photoScale = baja::rand(0.4f, 0.9f);
        std::wstring photoFileName = (boost::wformat(L"%i.jpg") % (photoIndex % sourcePhotoCount)).str();

        addPhoto(
            notebookView,
            pageId,
            photoFileName,
            photoAngle,
            photoX,
            photoY,
            photoScale);

        // this sleep is necessary to allow the async file copying to release all locks.
        sleep(postPhotoPause);
    }
}

void automation_service::runPageStressTest(std::shared_ptr<journal::view::inotebook> notebookView)
{
    b::uint32 pageCount = constants::page_test_automation_settings::pageCount();
    b::uint32 strokeCountPerPage = constants::page_test_automation_settings::strokeCountPerPage();
    b::uint32 textIndex = 0;

    for (b::uint32 pageNumber = 0 ; pageNumber < pageCount; ++pageNumber)
    {
        DEBUG_CONSOLE_TRACE(boost::wformat(L"automation: adding page %i out of %i") % pageNumber % pageCount);
        b::uint32 pageId = notebookView->addNewPage();

        fillPageWithPhotos(
            notebookView,
            pageId);

        textIndex = 
            fillPageWithStrokes(
                notebookView,
                pageId,
                getTestText(),
                textIndex,
                strokeCountPerPage,
                constants::page_test_automation_settings::postSymbolPause(),
                constants::page_test_automation_settings::fontSize(),
                constants::page_test_automation_settings::fontThickness());

        sleep(constants::page_test_automation_settings::postIterationPause());
    }
}

void automation_service::runStrokeStressTest(std::shared_ptr<journal::view::inotebook> notebookView)
{
    b::uint32 pageId = notebookView->addNewPage();
    b::uint32 strokeCountPerPage = constants::stroke_test_automation_settings::strokeCount();
    b::uint32 textIndex = 0;

    fillPageWithStrokes(
        notebookView,
        pageId,
        getTestText(),
        textIndex,
        strokeCountPerPage,
        constants::stroke_test_automation_settings::postSymbolPause(),
        constants::stroke_test_automation_settings::fontSize(),
        constants::stroke_test_automation_settings::fontThickness());
}

void automation_service::showCredits()
{
    raiiBool b(&_inProgress);
    _inProgress = true;

    std::shared_ptr<journal::view::inotebook> notebookView = _notebookView.lock();
    if ( notebookView == nullptr)
    {
        return;
    }

    b::uint32 pageId = notebookView->addNewPage();
    std::shared_ptr<view::inotebook_automation> notebookAutomation = std::dynamic_pointer_cast<view::inotebook_automation>(notebookView);
    std::shared_ptr<view::iink_surface> inkSurface = getInkSurface(notebookAutomation->pageFromId(pageId));

    static b::uint32 signature = 0;
    switch(signature)
    {
    case 0:
        {
            symbol_context symbolContext;
            symbolContext.default = false;
            symbolContext.scale = 1.0f;

            symbolContext.x = -2.0f;
            symbolContext.y = -2.0f;

            inkSymbol(L"jorge", symbolContext, inkSurface);

            symbolContext.x = -1.0f;
            symbolContext.y = -1.0f;
            inkSymbol(L"eric", symbolContext, inkSurface);

            symbolContext.x = -2.0f;
            symbolContext.y = 0.0f;
            inkSymbol(L"alan", symbolContext, inkSurface);

            symbolContext.x = 0.0f;
            symbolContext.y = 1.0f;
            inkSymbol(L"julio", symbolContext, inkSurface);

			symbolContext.x = 0.0f;
            symbolContext.y = 3.0f;
            inkSymbol(L"george", symbolContext, inkSurface);
        }
        break;
    case 1:
        {
            symbol_context symbolContext;
            symbolContext.default = false;
            symbolContext.x = -2.0f;
            symbolContext.y = 1.0f;
            inkSymbol(L"jorge_sketch_0", symbolContext, inkSurface);
        }
        break;
    case 2:
        {
            symbol_context symbolContext;
            symbolContext.default = false;
            symbolContext.x = -2.0f;
            symbolContext.y = 1.0f;
            inkSymbol(L"jorge_sketch_1", symbolContext, inkSurface);
        }
        break;
    }
    signature = ++signature % 3;
}

void automation_service::showCreditsAsync()
{
    concurrency::task<void> t(
            [this]()
            {
                showCredits();
            }
        );
}

std::shared_ptr<view::iink_surface> automation_service::getInkSurface(
    const std::shared_ptr<view::ipage>& page)
{
    std::shared_ptr<view::ipage_automation> pageAutomation = std::dynamic_pointer_cast<view::ipage_automation>(page);

    std::shared_ptr<view::iink_surface> inkSurface = pageAutomation->inkSurface();
    do
    {
        sleep(50);
        inkSurface = pageAutomation->inkSurface();
    }while (inkSurface == nullptr);

    return inkSurface;
}

void automation_service::runEnabledTests()
{
    std::shared_ptr<journal::view::inotebook> notebookView = _notebookView.lock();
    if ( notebookView == nullptr)
    {
        return;
    }
    if (constants::test_automation_settings::pageStressTestEnabled())
    {
        this->runPageStressTest(notebookView);
    }

    if (constants::test_automation_settings::strokeStressTestEnabled())
    {
        this->runStrokeStressTest(notebookView);
    }
}

void automation_service::runEnabledTestsAsync()
{
    concurrency::task<void> t(
            [this]()
            {
                runEnabledTests();
            }
        );
}

std::wstring automation_service::deviceTypeString(a::device_type dt)
{
    std::wstring deviceType;
    switch (dt)
    {
    case a::device_type::pen:
        deviceType = L"a::device_type::pen";
        break;
    case a::device_type::mouse:
        deviceType = L"a::device_type::mouse";
        break;
    case a::device_type::touch:
        deviceType = L"a::device_type::touch";
        break;
    };

    return deviceType;
}

std::wstring automation_service::pointerEventString(a::pointer_event_type pe)
{
    std::wstring pointerEvent;
    switch (pe)
    {
    case a::pointer_event_type::down:
        pointerEvent = L"a::pointer_event_type::down";
        break;
    case a::pointer_event_type::move:
        pointerEvent = L"a::pointer_event_type::move";
        break;
    case a::pointer_event_type::up:
        pointerEvent = L"a::pointer_event_type::up";
        break;
    case a::pointer_event_type::wheel:
        pointerEvent = L"a::pointer_event_type::wheel";
        break;
    };

    return pointerEvent;
}

std::wstring automation_service::keyModifiersString(a::key_modifiers km)
{
    std::wstring keyModifiers;
    switch (km)
    {
    case a::key_modifiers::none:
        keyModifiers = L"a::key_modifiers::none";
        break;
    case a::key_modifiers::control:
        keyModifiers = L"a::key_modifiers::control";
        break;
    case a::key_modifiers::menu:
        keyModifiers = L"a::key_modifiers::menu";
        break;
    case a::key_modifiers::shift:
        keyModifiers = L"a::key_modifiers::shift";
        break;
    case a::key_modifiers::windows:
        keyModifiers = L"a::key_modifiers::windows";
        break;
    };

    return keyModifiers;
}

std::wstring automation_service::colorString(b::uint32 c)
{
    std::wstring color;
    switch (c)
    {
    case journal::constants::ink::ink_color::black:
        color = L"journal::constants::ink::ink_color::black";
        break;
    case journal::constants::ink::ink_color::red:
        color = L"journal::constants::ink::ink_color::red";
        break;
    case journal::constants::ink::ink_color::highlighter:
        color = L"journal::constants::ink::ink_color::highlighter";
        break;
    case journal::constants::ink::ink_color::pressure:
        color = L"journal::constants::ink::ink_color::pressure";
        break;
    case journal::constants::ink::ink_color::blue:
        color = L"journal::constants::ink::ink_color::blue";
        break;
    case journal::constants::ink::ink_color::green:
        color = L"journal::constants::ink::ink_color::green";
        break;
    case journal::constants::ink::ink_color::pink:
        color = L"journal::constants::ink::ink_color::pink";
        break;
    }
    return color;
}

void automation_service::normalizeSymbolPosition(const std::vector<symbol_strokes>& strokes, const bool& dangling)
{
    b::float32 pageWidth = constants::general::pageWorldDimensions().x;
    b::float32 pageHeight = constants::general::pageWorldDimensions().y;
    b::float32 minx = pageWidth / 2;
    b::float32 miny = pageHeight / 2;
    b::float32 maxx = - minx ;
    b::float32 maxy = - miny;

    for (auto &stroke : strokes)
    {
        for (b::uint32 i = 0 ; i < stroke.count ; ++i)
        {
            if (stroke.points[i].position.x > maxx)
            {
                maxx = stroke.points[i].position.x;
            }
            if (stroke.points[i].position.x < minx)
            {
                minx = stroke.points[i].position.x;
            }
            if (stroke.points[i].position.y > maxy)
            {
                maxy = stroke.points[i].position.y;
            }
            if (stroke.points[i].position.y < miny)
            {
                miny = stroke.points[i].position.y;
            }
        }
    }

    b::float32 symbolDeltaX = minx; // move it to the right of the y axis.
    b::float32 symbolDeltaY = 0;
    if (dangling == false)
    {
        symbolDeltaY = miny;
    }
    else
    {
        symbolDeltaY = (miny + maxy) / 2;
    }

    for (auto &stroke : strokes)
    {
        for (b::uint32 i = 0 ; i < stroke.count; ++i)
        {
            stroke.points[i].position.x -= symbolDeltaX;
            stroke.points[i].position.y -= symbolDeltaY;
        }
    }
}

std::shared_ptr<iautomation_service> journal::services::automation::createAutomationService()
{
    return automation_service::create();
}

}}} // journal::services::automation

